﻿#region Copyright

// The contents of this file are subject to the Mozilla Public License
//  Version 1.1 (the "License"); you may not use this file except in compliance
//  with the License. You may obtain a copy of the License at
//  
//  http://www.mozilla.org/MPL/
//  
//  Software distributed under the License is distributed on an "AS IS"
//  basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
//  License for the specific language governing rights and limitations under 
//  the License.
//  
//  The Initial Developer of the Original Code is Robert Smyth.
//  Portions created by Robert Smyth are Copyright (C) 2008.
//  
//  All Rights Reserved.

#endregion

using System;
using System.IO;
using System.Text;
using NUnitGrid.Framework.Logging;


namespace NUnitGrid.Framework.Pipes.States
{
    public class ConnectedState : IPipeState, IScheduledEventListener
    {
        private readonly IApplicationMessageListener applicationMessageListener;
        private readonly TimeSpan linkIdleTimeLimit = TimeSpan.FromSeconds(20);
        private readonly ILinkReadyListener linkReadyListener;
        private readonly IPipeListener listener;
        private readonly ILogger logger;
        private readonly TimeSpan pollPeriod;
        private readonly ISchedule schedule;
        private readonly IStateEngine<IPipeState> stateEngine;
        private readonly IPipeStateFactory stateFactory;
        private readonly IPipeStream stream;
        private readonly TimeSpan tickPeriod = TimeSpan.FromSeconds(1);
        private DateTime lastReceivedMessageTime = DateTime.Now;
        private DateTime lastSentMessageTime = DateTime.MinValue;
        private StringBuilder receivedLinesBuffer = new StringBuilder();

        public ConnectedState(IStateEngine<IPipeState> stateEngine, IPipeStateFactory stateFactory, ISchedule schedule,
                              ILogger logger, IPipeStream stream, IApplicationMessageListener applicationMessageListener,
                              IPipeListener listener, ILinkReadyListener linkReadyListener, TimeSpan pollPeriod)
        {
            this.stateEngine = stateEngine;
            this.stateFactory = stateFactory;
            this.schedule = schedule;
            this.logger = logger;
            this.stream = stream;
            this.applicationMessageListener = applicationMessageListener;
            this.listener = listener;
            this.linkReadyListener = linkReadyListener;
            this.pollPeriod = pollPeriod;
        }

        public virtual void OnEntering()
        {
            logger.Info.Write("Pipe connected");

            lastReceivedMessageTime = DateTime.Now;

            stream.Start();
            schedule.AddEvent(this, tickPeriod);

            linkReadyListener.OnStateChange(ChannelState.Connected);
            listener.OnConnectedChanged(true);
        }

        public virtual void Connect()
        {
            throw new InvalidOperationException("Cannot connect when already connected");
        }

        public virtual void SendMessage(string message)
        {
            try
            {
                logger.Verbose.Write("Sending message: {0}", message);

                lastSentMessageTime = DateTime.Now;

                var wrappedMessage = new StringBuilder();
                wrappedMessage.AppendLine("Link.AppMessage()");
                wrappedMessage.AppendLine("{");

                var lines = message.Split('\n');
                foreach (var line in lines)
                {
                    wrappedMessage.AppendLine(string.Format(" {0}", line.Trim('\r')));
                }

                wrappedMessage.AppendLine("}");

                stream.WriteLine(wrappedMessage.ToString());
            }
            catch (IOException)
            {
                logger.Warning.Write("Error sending messsage");
                TransitionToConnectingState();
            }
        }

        public virtual void OnReceivedLine(string message)
        {
            logger.Verbose.Write("Received message: {0}", message);

            lastReceivedMessageTime = DateTime.Now;

            var lines = message.Split('\n');

            foreach (var line in lines)
            {
                var receivedLine = line.Trim('\r');
                receivedLinesBuffer.AppendLine(receivedLine);

                if (receivedLinesBuffer.Length >= 100000)
                {
                    throw new GridAppProtocolException("Received buffer overflow");
                }

                if (receivedLine.Length > 0 && receivedLine[0] == '}')
                {
                    try
                    {
                        var receivedMessage = receivedLinesBuffer.ToString().Trim('\r', '\n');
                        receivedLinesBuffer = new StringBuilder(64*1024);

                        ParseReceivedMessage(receivedMessage);
                    }
                    catch (Exception exception)
                    {
                        throw new GridAppProtocolException(
                            string.Format("Error '{0}' parsing message:\n{1}", exception, message));
                    }
                }
            }
        }

        public void Stop()
        {
            stream.Stop();
        }

        void IScheduledEventListener.OnEventFired()
        {
            if (DateTime.Now >= lastReceivedMessageTime + linkIdleTimeLimit)
            {
                logger.Warning.Write("Link timeout");
                TransitionToConnectingState();
            }
            else
            {
                if (DateTime.Now >= lastSentMessageTime + pollPeriod)
                {
                    SendLinkPoll();
                }
                schedule.AddEvent(this, tickPeriod);
            }
        }

        private void ParseReceivedMessage(string receivedMessage)
        {
            if (receivedMessage.StartsWith("Link.AppMessage("))
            {
                HandleAppMessage(receivedMessage);
            }
            else if (receivedMessage.StartsWith("Link."))
            {
                if (receivedMessage.StartsWith("Link.Poll("))
                {
                    stream.WriteLine("Link.Ack()\n{\n}");
                }
                else if (!receivedMessage.StartsWith("Link.Ack("))
                {
                    throw new GridAppProtocolException(string.Format("Invalid link message format '{0}'",
                                                                     receivedMessage));
                }
            }
            else
            {
                throw new GridAppProtocolException(string.Format("Invalid command format '{0}'", receivedMessage));
            }
        }

        protected virtual void TransitionToConnectingState()
        {
            schedule.RemoveEventsFor(this);
            stream.Stop();
            stateEngine.ChangeState(stateFactory.CreateConnectingState());
            listener.OnConnectedChanged(false);
            linkReadyListener.OnStateChange(ChannelState.NotConnected);
        }

        private void SendLinkPoll()
        {
            logger.Verbose.Write("Sending poll messsage");

            try
            {
                lastSentMessageTime = DateTime.Now;
                stream.WriteLine("Link.Poll()\n\r{\n\r}");
            }
            catch (IOException exception)
            {
                logger.Warning.Write("Error sending link poll {0}", exception);
            }
        }

        protected void HandleAppMessage(string message)
        {
            message = message.Remove(0, message.IndexOf('{') + 1);
            var lastClosingBraceIndex = message.LastIndexOf('}');
            message = message.Remove(lastClosingBraceIndex, message.Length - lastClosingBraceIndex);
            message = message.Trim();
            message = message.Trim('\n', '\r');

            applicationMessageListener.OnMessageReceived(message);
        }
    }
}